feat(a2a)!: checkpoint API + harden context-id retention#258
Merged
Conversation
Follow-up to PR 251 addressing a post-merge audit. Highlights:
- **Ordering fix**: `_context_id` and `_active_task_id` now commit only
after `_process_task_response` and the idempotency-error check both
succeed. Previously, a raise from either would leave the adapter
advanced to reflect a response the caller never received, potentially
orphaning the in-flight task on retry.
- **Rename**: `pending_task_id` → `active_task_id`. The name now matches
the semantic ("server-side task the next call must echo to resume").
- **Checkpoint API**: `ADCPClient.checkpoint()` returns a typed
`Checkpoint` (TypedDict with `agent_id`, `context_id`,
`active_task_id`). `ADCPClient.from_checkpoint(config, state)`
rehydrates both ids. Fixes the advertised Redis-resume story for HITL
flows — persisting only `context_id` would orphan the pending task.
Restore validates `agent_id` against the target config so a
checkpoint minted for Agent A can't leak session tokens to Agent B.
- **Enum coupling**: `_NONTERMINAL_TASK_STATES` is now
`frozenset[TaskState]`, so an upstream rename in a2a-sdk becomes a
type error instead of a silent retention regression.
- **`unknown` TaskState**: explicit — clears `active_task_id` and logs
a warning so operators notice if a server starts emitting it.
- **Empty-string guard**: `context_id=""` from `os.getenv(...) or ""`
patterns is now treated as "not provided" instead of being echoed on
the wire.
- **Coverage gaps**: added unit tests for `submitted`/`auth-required`
retention, `canceled`/`rejected`/`unknown` clearing, ordering
invariant under both raised and converted-to-FAILED exception paths,
and checkpoint mis-restore rejection.
BREAKING CHANGE: `ADCPClient.pending_task_id` is now
`ADCPClient.active_task_id` (same for `A2AAdapter`). The constructor's
`context_id=` kwarg and `reset_context()` now raise `TypeError` (was
`ValueError`) on non-A2A protocols — the string value is fine, the
operation doesn't apply to MCP.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4f4c36c to
a17ffb3
Compare
3 tasks
bokelley
added a commit
that referenced
this pull request
Apr 23, 2026
…te enum - Export ``Checkpoint`` TypedDict from the top-level ``adcp`` package so callers can ``from adcp import Checkpoint`` instead of reaching into ``adcp.client`` for the persistence-key type. - Compare ``task.status.state`` against ``TaskState.completed`` / ``TaskState.failed`` in ``_process_task_response`` to match the enum-based membership check in ``_NONTERMINAL_TASK_STATES`` — an upstream rename in a2a-sdk now fails as a type error instead of a silent classification drift. Also flips the in-progress release-please PR from 5.0.0 to 4.0.1. The prior commit (#258) landed breaking-style changes (``pending_task_id`` → ``active_task_id``, ``ValueError`` → ``TypeError`` on non-A2A) but 4.0.0 shipped under 48h earlier with no known A2A adopters — the salesagent migration is on MCP, and A2A adoption is deferred behind pluggable TaskStore / push-notification work. Treating those as pre-adoption corrections rather than a major bump. Release-As: 4.0.1 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #251 addressing the post-merge audit from @scr-oath, plus additional hardening from a code-review / security-review pass.
What changed
Correctness
a2a.py):_context_idand_active_task_idcommit only after_process_task_responseand the idempotency-error check both succeed. A raise from either previously left the adapter advanced to a response the caller never saw, potentially orphaning the in-flight task on retry.client.py):context_id=""fromos.getenv(...) or ""patterns is now treated as "not provided" instead of being seeded on the wire.unknownTaskState: explicit — clearsactive_task_idand logs a warning.API shape
pending_task_id→active_task_id(public onADCPClient+A2AAdapter, and internal_pending_task_id). The old name read as "queued, not yet sent"; the real semantic is "server-side task the next call must echo to resume."Checkpointis aTypedDictcarryingagent_id,context_id,active_task_id. Fixes the Redis-resume story for HITL flows — persisting onlycontext_idwould orphan the pending task.from_checkpointrejects mis-matchedagent_id(Agent A's checkpoint restored onto Agent B would leak session ids to a different vendor).ValueError→TypeErroron non-A2Acontext_id=/reset_context()/from_checkpoint(active_task_id=...). The string value is fine; the operation doesn't apply to non-A2A.Internals
_NONTERMINAL_TASK_STATES: frozenset[TaskState]— coupled to thea2a.types.TaskStateenum so an upstream rename becomes a type error, not a silent retention regression.A2AAdapter._restore_active_task_id()— narrow internal setter used only byfrom_checkpointso rename-safety fails loudly.Coverage
submitted,working,input-required,auth-required) and every terminal clear (completed,failed,canceled,rejected,unknown).IdempotencyConflictErrorpropagation path, one for the converted-to-TaskResult(FAILED)path.checkpoint()round-trip,agent_idmismatch rejection, and non-A2Aactive_task_idrejection.Breaking changes
ADCPClient.pending_task_id→ADCPClient.active_task_id(same forA2AAdapter).ADCPClient(context_id=...)/reset_context()raiseTypeError(wasValueError) on non-A2A.Both pre-GA; callers catching
ValueErroror readingpending_task_idmust update.Test plan
ruff check src/— cleanmypy src/adcp/— 679 files, cleanpytest tests/test_protocols.py— 70/70 passpytest tests/integration/test_a2a_context_id.py— 5/5 pass (real uvicorn-hosted a2a-sdk Starlette app)🤖 Generated with Claude Code